使用 Rxjs 替代 Redux(一)

Author Avatar
Splendour 4月 29, 2017

前言

在 Rxjs 中,一切都是流,我们订阅它们,并在它们改变的时候接收到最新的值。在 React 中,我们往组件传入参数,或者维护组件自身的状态,只要其中之一发生变化,组件就会重新渲染,达到页面更新的目的。结合一下,我们发现可以让组件订阅 Rxjs 的流,并且在流更新的时候刷新界面,这不就是单项数据流吗?

在 React 的使用过程中,我们通常使用 Redux 来管理一个全局的 Store,存储全局状态或数据,那我们是否可以用 Rxjs 来实现 Redux 的功能呢?

替换 Store

Redux 的 Store 首先是用来存储数据的(可通过官方文档详细了解)。结合之前的学习笔记,我们挑选了 BehaviorSubject 这个类来作为存储的数据流,因为它能实现数据共享,这也是 Store 中的数据能实现的。

定义与订阅

export const loading$ = new BehaviorSubject(false);

在这里我们定义了一个 loading$,存储加载状态的显示和隐藏。在项目中,变量后面加个 $ 表示这是属于一个流。

在 React 组件中,我们订阅它,并将它的状态写入到 state,这样,就可以在每次更新的时候修改组件的 state,从而实现组件更新。

一般,我们在 componentDidMount 中实现数据订阅。

this.subscriber = loading$.subscribe(loading => {
  this.setState({
    show: loading
  });
});

细心的同学会发现,订阅了之后,如果没有销毁,则该订阅会一直存在,数据源的变化会让这个订阅函数执行。这个时候如果组件已经不存在了,则会抛出异常(在一个不存在的组件中调用 setState 方法)。所以,我们需要在 componentWillUnmount 函数中,销毁该订阅。

this.subscriber.unsubscribe();

打印

使用 Redux 的时候,我们可以方便地通过 createLogger 中间件来实现 Store 的实时打印,方面开发过程中对数据的监控和调试。那么,如果我们使用了 Rxjs,该如何进行数据流实时状态的打印呢?其实思路很清晰,就是需要在数据流发生状态改变的时候,添加打印的逻辑即可。

Observable

Observable 提供了一个 do 方法

Intercepts each emission on the source and runs a function, but returns an output which is identical to the source

即提供了一个钩子,可以在 next、err 和 complete 三种状态发生时执行额外的操作,最终返回该 Observable 本身。所以,我们可以在 do 方法中,加入打印逻辑。

Observable.prototype.debug = function (name) {
  return this.do(
    (next) => {
      if (debuggerOn) {
        console.log(`%c ${name} emit value: `, `color: #4CAF50; font-weight: bold`, next);
      }
    },
    (err) => {
      if (debuggerOn) {
        console.error(`${name} throw ERROR >>> `, err);
      }
    },
    () => {
      if (debuggerOn) {
        console.log(`${name} completed.`);
      }
    }
  );
};

即拓展了 Observable,让它能链式调用 debug 方法,传入名称,实现打印。

export const loading$ = Observable.of(false).debug('[common]:[loading]');
BehaviorSubject

如果是 BehaviorSubject 呢?我们尝试一下可以发现,如果将 BehaviorSubject 也调用 debug 方法,则会产生类型的转换,变成了普通的 Observable,使用 BehaviorSubject 的优势没了。查阅文档也没发现 BehaviorSubject 有类似 do 的方法,那么我们需要换个思路,自己实现类似的方法。

既然我们是需要在数据变化的时候打印,那么我们相当于要订阅这份数据,在不同状态下实现打印逻辑即可。从 do 函数的实现思路出发,我们可以订阅 BehaviorSubject 自身,实现打印逻辑后,返回本身,实现链式调用,所以我们可以拓展这样的一个 debug 方法。

BehaviorSubject.prototype.debug = function (name) {
  this.subscribe((next) => {
      if (debuggerOn) {
        console.log(`%c ${name} emit value: `, `color: #4CAF50; font-weight: bold`, next);
      }
    },
    (err) => {
      if (debuggerOn) {
        console.error(`${name} throw ERROR >>> `, err);
      }
    },
    () => {
      if (debuggerOn) {
        console.log(`${name} completed.`);
      }
    });
  return this;
};

使用也非常简单

export const loading$ = new BehaviorSubject(false).debug('[common]:[loading]');

实现 Action

我们上面仅仅是实现了数据的至上而下传递。在 Redux 中,由用户操作等逻辑,组件本身需要修改 Store,是通过发送一个 Action 实现的;对于 Rxjs 来说,Store 层也需要提供相应的 Action,让组件可以调用,使得对应的数据流能 emit 新的数据,组件订阅的数据更新,从而实现组件的刷新。

这里使用很简便的方式来实现

export const showLoading = () => {
  loading$.next(true);
};

export const hideLoading = () => {
  loading$.next(false);
};

组件内部,在需要的地方,调用即可。

总结

通过以上的方式,我们可以很方便地使用 Rxjs 来替代 Redux,实现前端的数据层和数据传递。这些都是属于同步操作,那么,如何用 Rxjs 实现异步操作呢?下一篇文章将会继续分享。